WLJS Functions
Quick and Dirty
Create a new cell, then define a function inside the core
context:
.js
core.MyFunction = async (args, env) => {
const data = await interpretate(args[0], env);
alert(data);
}
To call it directly from the Wolfram Kernel, use:
MyFunction["Hello World!"] // FrontSubmit;
Or in the cell's output:
CreateFrontEndObject[MyFunction["Hello World!"]]
This should produce a pop-up message with the given text.
A Deeper Look
First, one needs to understand how the function is called. There are a few different ways:
- Direct calls (as shown above)
- Indirect calls using containers
The first one is straightforward, and most UI and core built-in functions are written this way.
Simple Direct Call
As simple as this:
A called function has no persistent memory (call and forget), except for the env
variable, which can share data with other functions in the same subtree.
Any defined function must return a JavaScript object or nothing.
Arguments (args
) are always Wolfram Expressions, so use interpretate
to convert them into JavaScript data types.
For example, env
is very handy when working with graphics:
core.RGBColor = async (args, env) => {
const r = await interpretate(args[0], env);
const g = await interpretate(args[1], env);
const b = await interpretate(args[2], env);
// Modify env
env.color = new whatever.Color(r, g, b);
// Return in case it's used directly
return env.color;
};
You can then call core.RGBColor
somewhere in a list:
{Blue[], {Red[], Sphere[] (* I am red *)}, Cuboid[] (* I am blue *)} // Graphics3D
In the parent function:
core.Graphics3D = async (args, env) => {
...
const scene = {};
await interpretate(args[0], { ...env, scene });
rendered.add(scene); // All objects are there!
};
core.Sphere = async (args, env) => {
...
env.scene.add(new sphere({ color: env.color }));
};
Thus, the desired data ends up in the scene. This is the power of pointers.
🎡 Example 1: Game of Life
To have a bit of fun, let's define some supporting structures.
Create a new cell with the following code:
.js
// Create JS canvas
const canvas = document.createElement("canvas");
canvas.width = 400;
canvas.height = 400;
let context = canvas.getContext("2d");
context.fillStyle = "lightgray";
context.fillRect(0, 0, 500, 500);
// An array to store the previous state
let old = new Array(40);
for (let i = 0; i < old.length; i++) {
old[i] = new Array(40).fill(0);
}
// A function to draw on it
core.MyFunction = async (args, env) => {
const data = await interpretate(args[0], env);
// Draw our boxes
for (let i = 0; i < 40; ++i) {
for (let j = 0; j < 40; ++j) {
// Old pixels leave blue traces
if (old[i][j] > 0) {
context.fillStyle = "rgba(0,0,255,0.2)";
context.fillRect(i * 10 + 2, j * 10 + 2, 6, 6);
}
// New pixels
if (data[i][j] > 0) {
context.fillStyle = "rgba(255,0,0,0.4)";
context.fillRect(i * 10 + 1, j * 10 + 1, 8, 8);
} else {
context.fillStyle = "rgba(255,255,255,0.4)";
context.fillRect(i * 10 + 1, j * 10 + 1, 8, 8);
}
// Store the previous frame
old[i][j] = data[i][j];
}
}
};
return canvas;
This function will draw a 40x40 array of 1 and 0 pixels on the page. Let's try it out with a typical cellular automaton like Conway's Game of Life:
gameOfLife = {224, {2, {{2, 2, 2}, {2, 1, 2}, {2, 2, 2}}}, {1, 1}};
board = RandomInteger[1, {40, 40}];
Do[
board = CellularAutomaton[gameOfLife, board, {{0, 1}}] // Last;
MyFunction[board] // FrontSubmit;
Pause[0.1];
, {i, 1, 100}]
The output is shown in the GIF below, but you can try it yourself by opening GOL.wln
from the examples folder.
See more about JS cells in Cell Types
Of course, this is not the most efficient way to make animations. A better method would be to let JS run the animation on its own and wait for or request new data using a system of events.
A Remark About Subsymbols | Methods
Sometimes a function or a tree of functions is called in response to some event. This information is stored in env.method
. See also Symbols.
If there is a data update, i.e. env.method = 'update'
, then the interpreter will try to find a subsymbol or function to call (read more about how the interpreter handles them in WLJS Interpreter).
Imagine if every defined function was like a class with a constructor and methods (subsymbols):
core.MyFunction = (args, env) => {}
core.MyFunction.update = (args, env) => {}
core.MyFunction.destroy = (args, env) => {}
core.MyFunction.whatever = (args, env) => {}
Then, you can specify which method to call during evaluation:
core.OurParentFunction = (args, env) => {
const data = interpretate(args[0], { ...env, method: 'update' });
...
}
This is extremely useful for updating graphs or plots. Instead of redrawing everything, you can perform lightweight operations:
core.ListLinePlotly = (args, env) => {
// Create the element, draw axes, etc... (SLOW)
Plotly.newPlot(...);
};
core.ListLinePlotly.update = (args, env) => {
// Quickly redraw only changed elements
Plotly.animate(...) or extend...
};
Destroy and update methods are usually features of Virtual Functions.
Contexts
To prevent name collisions and clutter, you can specify a context (or library) of symbols using:
var library = {};
library.name = "It is important to write a name";
interpretate.extendContext(library);
library.Symbol = (args, env) => {
// ...
};
The interpreter will check all available contexts and use the first match. However, you can prioritize the context by providing it in the env
object:
core.GrantSymbol = (args, env) => {
const data = interpretate(args, { ...env, context: library });
// ...
// First match should be in the `library` object
};
If you have multiple prioritized contexts, you can pass them as an array:
core.GrantSymbol = (args, env) => {
const data = interpretate(args, { ...env, context: [library1, library2] });
// ...
// First match in `library1`, if not found, then in `library2`, and finally in the `core` context
};
Virtualization
Each time the interpreter encounters a virtual function (or call it a symbol), it creates a unique object that scopes the env
variable and provides local memory for all Wolfram Expressions located inside the container.
The interpreter can automatically create a container for any WLJS symbol when it sees a special property defined:
core.MySymbol.virtual = true;
Then MySymbol
takes advantage of virtualization behavior, even if it’s called anonymously via FrontSubmit.
The most important features of virtual functions:
- They can be destroyed or updated (see Subsymbols | Methods)
- They automatically bind to child virtual functions instances, enabling reevaluation when changes are made in children (foundation for Dynamics)
Properties
Virtualized execution is powerful because each call creates a separate instance with local memory. The env
variable provides access to:
core.MyFunction = (args, env) => {
env.local // local memory for this instance
env.global // global memory for the call tree
env.exposed // memory shared outside the instance
env // sharable memory within the instance
};
- Global memory is created with each widget or scope via
FrontSubmit
. - Local memory is specific to each instance.
- When executed in the output cell (aka ViewBox) or on a slide,
env.element
provides access to the DOM placeholder.
Let’s look at an example using local memory:
🎡 Example 2: Clocks
.js
core.PlaceholderClock = async (args, env) => {
env.local.start = new Date();
env.local.clock = setInterval(() => {
const d = new Date() - env.local.start;
env.element.innerHTML = d;
}, 10);
};
core.PlaceholderClock.destroy = async (args, env) => {
clearInterval(env.local.clock);
const passed = new Date() - env.local.start;
alert(`${passed}ms passed`);
};
core.PlaceholderClock.virtual = true;
Then we can use it like this using ViewBox to execute Javascript
PlaceholderClock /: MakeBoxes[p_PlaceholderClock, StandardForm] := ViewBox[p, p];
PlaceholderClock[]
Here the first argument of ViewBox
is actual input form of the expression, while the second one will be used to render the view, which covers input form in the editor.
Copy/paste it across cells. When you delete a widget, .destroy
runs and stops the clock showing a pop up message
This is especially useful for things like Graphics3D
, where a rendering function might run 60 times per second. You don’t want it to keep running if the widget is removed:
core.Graphics3D.destroy = async (args, env) => {
cancelAnimationFrame(env.local.aid);
};
Default Methods
Users can define their own subsymbols (methods) via env.method
.
For container-based functions, you should define these:
.destroy
— triggered when a widget is removed (runs top-down).update
— triggered when data changes (runs bottom-up)
core.MyFunction.update = async (args, env) => {};
core.MyFunction.destroy = async (args, env) => {};
Without these, deleting a widget can cause errors.
A Note on Packed and Numeric Arrays
When sending large chunks of numeric data, the Wolfram Kernel may use PackedArray
, which becomes a NumericArrayObject
in JavaScript (a wrapper for TypedArray
).
Always check for this:
let data = await interpretate(args[0], env);
if (data instanceof NumericArrayObject) {
// handle TypedArray
} else {
// handle normal array
}
You can convert to a normal JS array with:
data = data.normal();
🎡 Example 3: Game of Life (Improved)
Let’s enhance our animation with containers:
.js
core.MyFunction = async (args, env) => {
let data = await interpretate(args[0], env);
if (data instanceof NumericArrayObject) {
data = data.normal();
}
const canvas = document.createElement("canvas");
canvas.width = 400;
canvas.height = 400;
let context = canvas.getContext("2d");
context.fillStyle = "lightgray";
context.fillRect(0, 0, 500, 500);
let old = new Array(40);
for (let i = 0; i < old.length; i++) {
old[i] = new Array(40).fill(0);
}
env.element.appendChild(canvas);
env.local.old = old;
env.local.ctx = context;
};
core.MyFunction.virtual = true;
Update method:
.js
core.MyFunction.update = async (args, env) => {
let data = await interpretate(args[0], env);
if (data instanceof NumericArrayObject) {
data = data.normal();
}
const context = env.local.ctx;
for (let i = 0; i < 40; ++i) {
for (let j = 0; j < 40; ++j) {
if (env.local.old[i][j] > 0) {
context.fillStyle = "rgba(0,0,255,0.2)";
context.fillRect(i * 10 + 2, j * 10 + 2, 6, 6);
}
if (data[i][j] > 0) {
context.fillStyle = "rgba(255,0,0,0.4)";
context.fillRect(i * 10 + 1, j * 10 + 1, 8, 8);
} else {
context.fillStyle = "rgba(255,255,255,0.4)";
context.fillRect(i * 10 + 1, j * 10 + 1, 8, 8);
}
}
}
env.local.old = data;
};
core.MyFunction.destroy = (args, env) => {};
Let us try to use it with a defined output forms
gameOfLife = {224, {2, {{2, 2, 2}, {2, 1, 2}, {2, 2, 2}}}, {1, 1}};
board = RandomInteger[1, {40, 40}];
MyFunction /: MakeBoxes[m_MyFunction, StandardForm | WLXForm] := ViewBox[m,m]
and then create out first instance
MyFunction[board]
It will show you a blank gray screen, since core.MyFunction
only draws the data when it changes. To bind MyFunction
to board
symbol, we need to hold the last one from the evaluation. This can be done using Offload
MyFunction[board // Offload]
See more about it in Dynamic Symbols
When board
changes:
Do[
board = CellularAutomaton[gameOfLife, board, {{0, 1}}] // Last;
Pause[0.1];
, {i, 1, 100}]
All instances update live! Even if you copy and paste the same expressions, its display will be unique
Dynamic Symbols
As one could see from the previous example
board
symbol triggers update of our GOL canvas.board
can be classified exactly asdynamic symbol
Any defined Wolfram Language symbol with an OwnValue
like:
radius = 1;
Wrapped in Offload becomes a core.radius
virtual symbol:
Graphics[Disk[{0., 0.}, Offload[radius]]]
The Kernel tracks changes to OwnValues
, so if Disk
is also virtual, both are coupled. For example:
EventHandler[InputRange[0, 1, 0.1], Function[r, radius = r]]
Will trigger .update
on Disk
.
Injection into a Virtual Instance
Sometimes you want to add new objects into an existing instance (like a Graphics
scene) without reevaluating everything. You can do this using FrontInstanceReference + FrontSubmit.
🎡 Example with Lines
Create a reference to an instance:
ref = FrontInstanceReference[];
Plot[x, {x, 0, 1}, Epilog -> {ref}]
Then inject a new object:
FrontSubmit[Line[{{0.2, 0.6}, {0.1, 0.5}}], ref]
See FrontSubmit and FrontInstanceReference for more.
Frontend Objects
If you copy and paste any expressions from the given examples to a normal text editor, it will reveal InputForm
(*VB[*)(MyFunction[Offload[board]])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRAeF5DwrXQrzUsuyczPg4ixAwn/tLSc/MSUYlYgOyk/sSgFAL6LDrY="*)(*]VB*)
What if the inner expressions are too big to be displayed? For example Graphics3D can include 10000 of polygons. To cope with this problem you can wrap it into Frontend Objects, i.e.
MyFunction /: MakeBoxes[m_MyFunction, StandardForm | WLXForm] := With[{
o = CreateFrontEndObject[m]
},
ViewBox[Null, o]
]
then you get
(*VB[*)(Null)(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeEJBwK8rPK3HNS3GtSE0uLUlMykkNVgEKJ5oaGBukpBnomlqYGOmapFgY6CZamJnoGllYmJukGRqkWpqaAAB12BS3"*)(*]VB*)
- expression is compressed to JSON and will be loaded via separate channel
- expression no longer keeps its original form and will return
Null
To improve this situation, one can use a standard form of FrontEndExecutable
MyFunction /: MakeBoxes[m_MyFunction, form: StandardForm | WLXForm] := With[{
o = CreateFrontEndObject[m]
},
MakeBoxes[o, form]
]
Then we get
(*VB[*)(FrontEndRef["382744b2-d98e-49ef-9858-d15675873a2b"])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeEJBwK8rPK3HNS3GtSE0uLUlMykkNVgEKG1sYmZuYJBnpplhapOqaWKam6VpamFrophiampmbWpgbJxolAQB3iBUG"*)(*]VB*)
- expression is compressed to JSON and will be loaded via separate channel
- expression keeps its original form, since FrontEndRef directly points to the internal storage of the original expression